<template>
  <div
    id="diagramContainer"
    class="flowchart-container"
    @mousedown="onMouseDown"
    @mousemove="onMouseMove"
    @mouseup="onMouseUp"
  >
    <!-- <context-menu-on-node></context-menu-on-node> -->
    <node-component
      :id="item.elID"
      :style="{ left: item.left + 'px', top: item.top + 'px' }"
      :key="item.id"
      :type="item.type"
      :paramValues="item.params"
      :nodeID="item.id"
      :flip="item.flip"
      :selected="selected(item.id)"
      v-for="item in items"
      class="item"
      label="node-component"
    ></node-component>
    <!-- key这个地方绝对不能乱写(比如简单地写成列表的索引)，一旦乱写就完蛋了 -->
    <mid-point
      :key="'midpoint-' + midpoint.edgeID + '-' + midpoint.index"
      :id="'midpoint-' + midpoint.edgeID + '-' + midpoint.index"
      :edge-id="midpoint.edgeID"
      :index="midpoint.index"
      v-for="midpoint in statusMgr.midpoints"
      class="midpoint"
    ></mid-point>
    <selection-mask
      :pos="selection"
      id="selection-mask"
      style="position: absolute"
    />
  </div>
</template>
<style>
.flowchart-container {
  width: 100%;
  height: 100%;
  position: relative;
}

.item {
  position: absolute;
}

.midpoint {
  position: absolute;
}

.port-label {
  font-size: 10px;
  user-select: none;
}

.highlight-block {
  /* color: #fff; */
  position: absolute;
  width: 30px;
  height: 30px;
  margin-top: -11px;
  margin-left: -11px;
  border-radius: 50%;
  /* background-color: #f56c6c; */
  opacity: 0.3;
  /* animation: fade 1000ms infinite; */

  /* -webkit-animation: fade 1000ms infinite; */
}
</style>

<script lang="ts">
import { getCardByType, getCards, run } from "@/api/cards";
import { defineComponent } from "@vue/runtime-core";
import { currentColorScheme } from "@/colorscheme/colorscheme";
import {
  Connection,
  ConnectionMadeEventInfo,
  jsPlumb,
  jsPlumbInstance,
} from "jsplumb";
import { reactive, ref } from "vue";
import { parseEndpointID, portToEndpointUUID, toPortUUID } from "./utils";
import NodeComponent from "./NodeCompnent.vue";
import { Edge, Midpoint, Node, Param } from "./models";
import { addNode, dumpFlowchart, addMidpoint } from "./io";
import MidPoint from "./MidPoint.vue";
import { StatusManager } from "./statusmgr";
import { onEdgeClick, bindConnEvents } from "./events";
import {
  setCanvasOnNewNodeCallback,
  addOnNodeParamsChanged,
  setOnHighlightPort,
  setOnNodeDeleteRequest,
  setOnNodeFlipped,
  setOnRun,
  setOnEditPortsOnNode,
} from "@/components/EventBus";
// import ContextMenuOnNode from "@/components/panels/ContextMenuOnNode.vue";
import SelectionMask from "./SelectionMask.vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { tr } from "@/assets/translation/trans";
export default defineComponent({
  components: { NodeComponent, MidPoint, SelectionMask },
  props: {},
  setup() {
    let plumbInstance: jsPlumbInstance = {} as jsPlumbInstance;
    const items: Node[] = reactive([]);
    const midpoints: Midpoint[] = reactive([]);
    const statusMgr = reactive(new StatusManager());
    const loadFinished = false;
    const loadErrorOccured = false;
    return {
      plumbInstance,
      items,
      midpoints,
      statusMgr,
      loadFinished,
      createdAt: Date.now(),
      unmounted: false,
      loadErrorOccured,
      selection: reactive({
        startLeft: 0,
        startTop: 0,
        endLeft: 200,
        endTop: 200,
        visible: false,
      }),
      selectedItems: reactive(new Set<string>()),
      selectionEnabled: ref(false),
    };
  },
  methods: {
    getNode(id: string) {
      const node: Node = this.items.find((node: Node) => node.id == id) as Node;
      if (node == null) {
        throw Error(`node with id ${id} is empty.`);
      } else {
        return node;
      }
    },
    async addNode(
      id: string,
      left: number,
      top: number,
      type: string,
      params: Param[],
      flip: boolean,
      inputPorts: string[],
      outputPorts: string[]
    ) {
      const elID = id;
      this.items.push({
        id,
        elID,
        left,
        top,
        type,
        params,
        flip,
        inputPorts,
        outputPorts,
      });
      await this.$nextTick();
      await addNode(
        this.plumbInstance,
        this.statusMgr,
        elID,
        type,
        (_) => {
          const relatedEdges = this.statusMgr.getRelatedEdges(id, "all");
          for (const i in relatedEdges) {
            const edge = relatedEdges[i];
            this.statusMgr.fixEdgeConstraints(edge.id);
          }
          const node: Node = this.items.find(
            (node: Node) => node.id == id
          ) as Node;
          const elem: HTMLDivElement = document.getElementById(
            elID
          ) as HTMLDivElement;
          node.left = parseInt(elem.style.left.split("px")[0]);
          node.top = parseInt(elem.style.top.split("px")[0]);
        },
        (_) => {
          // Update the position of nodes.
          const node: Node = this.items.find(
            (node: Node) => node.id == id
          ) as Node;
          const elem: HTMLDivElement = document.getElementById(
            elID
          ) as HTMLDivElement;
          node.left = parseInt(elem.style.left.split("px")[0]);
          node.top = parseInt(elem.style.top.split("px")[0]);
        },
        flip
      );
    },
    async delNode(nodeID: string) {
      await this.statusMgr.delNode(this, nodeID);
      const index = this.items.findIndex((node: Node) => node.id == nodeID);
      if (index == -1) {
        console.error(this.items);
        throw Error(`node ${nodeID} not found!`);
      }
      this.items.splice(index, 1);
      await this.$nextTick();
    },
    // async onNodeClick(nodeID: string, evt: PointerEvent) {
    // if (evt.ctrlKey || evt.metaKey) {
    //   await this.delNode(nodeID);
    // }
    // },
    async addEdge(edge: Edge): Promise<boolean> {
      const uuidSrc = toPortUUID(edge.srcNode, "output", edge.srcEndpointID);

      const uuidDest = toPortUUID(edge.destNode, "input", edge.destEndpointID);

      const UUIDsToConnect: string[][] = [];
      let lastUUID = uuidSrc;
      for (let i = 0; i < edge.midpoints.length; i++) {
        const midpoint = edge.midpoints[i];
        if (midpoint.index != i) {
          console.error(edge.midpoints);
          throw Error("Midpoint index not match");
        }
        this.statusMgr.addMidpoint(midpoint);
        await this.$nextTick();

        const midpointUUID =
          "midpoint-" + midpoint.edgeID + "-" + midpoint.index;
        this.statusMgr.setMidpointPos(
          midpointUUID,
          midpoint.left,
          midpoint.top
        );

        addMidpoint(this.plumbInstance, this.statusMgr, midpointUUID);
        UUIDsToConnect.push([lastUUID, midpointUUID + "-input"]);
        lastUUID = midpointUUID + "-output";
      }
      UUIDsToConnect.push([lastUUID, uuidDest]);

      for (let i = 0; i < UUIDsToConnect.length; i++) {
        const conn = this.plumbInstance.connect({
          uuids: [UUIDsToConnect[i][0], UUIDsToConnect[i][1]],
        });
        if (conn != null) {
          bindConnEvents(conn);
        } else {
          console.error(
            "连接",
            UUIDsToConnect[i][0],
            UUIDsToConnect[i][1],
            "无法建立"
          );
          ElMessage.error(
            "连接" +
              UUIDsToConnect[i][0] +
              ":" +
              UUIDsToConnect[i][1] +
              "无法建立"
          );
          this.loadErrorOccured = true;
          return false;
        }
      }
      this.statusMgr.addEdge(edge);
      return true;
    },
    async save(): Promise<{ nodes: Node[]; edges: Edge[] }> {
      const dumped = dumpFlowchart(this.items, this.statusMgr);
      return dumped;
    },
    async load(json: { nodes: Node[]; edges: Edge[] }) {
      for (let i = 0; i < json.nodes.length; i++) {
        const node = json.nodes[i];
        await this.addNode(
          node.id,
          node.left,
          node.top,
          node.type,
          node.params,
          node.flip,
          node.inputPorts,
          node.outputPorts
        );
      }
      await this.$nextTick();
      for (let i = 0; i < json.edges.length; i++) {
        const edge = json.edges[i];
        const srcNode = this.statusMgr.getNode(edge.srcNode);
        const destNode = this.statusMgr.getNode(edge.destNode);
        // const srcCard = await getCardByType(srcNode.type);
        // const destCard = await getCardByType(destNode.type);
        const srcEndpointID = toPortUUID(
          srcNode.id,
          "output", // 源节点的输出端口
          edge.srcEndpointID
        );
        const destEndpointID = toPortUUID(
          destNode.id,
          "input", // 目标节点的输入端口
          edge.destEndpointID
        );
        if (
          this.statusMgr.endpointExists(srcEndpointID) &&
          this.statusMgr.endpointExists(destEndpointID)
        ) {
          await this.addEdge(edge);
        } else {
          console.error("Edge cannot be established:", edge);
        }
      }
    },
    async highlightEndpoint(endpointID: string) {
      const port = parseEndpointID(endpointID);
      const pos = this.statusMgr.getEndpointPosition(endpointID);
      const containerDiv = document.getElementById(
        "diagramContainer"
      ) as HTMLDivElement;
      const newElement = document.createElement("div") as HTMLDivElement;
      const bg = port.direction == "input" ? "#f56c6c" : "#6cf56c";
      newElement.className = "highlight-block";
      // newElement.style.zIndex = "-200";
      newElement.style.backgroundColor = bg;
      newElement.style.left = pos.left + "px";
      newElement.style.top = pos.top + "px";
      containerDiv.appendChild(newElement);
      window.setTimeout(() => {
        containerDiv.removeChild(newElement);
      }, 1000);
    },
    getScrollOffset(): { left: number; top: number } {
      const fcDiv = document.getElementById("diagramContainer")!;
      const left = fcDiv.scrollLeft;
      const top = fcDiv.scrollTop;
      return { left, top };
    },
    onMouseDown(evt: MouseEvent) {
      const { left, top } = this.getScrollOffset();
      this.selection.startLeft = evt.offsetX + left;
      this.selection.startTop = evt.offsetY + top;
      this.selection.endLeft = this.selection.startLeft;
      this.selection.endTop = this.selection.startTop;
      this.selectedItems.clear();
      this.selectionEnabled = true;
    },
    onMouseMove(evt: MouseEvent) {
      if (
        evt.buttons == 1 &&
        (evt.target as HTMLDivElement).id === "diagramContainer"
      ) {
        if (this.selectionEnabled && !this.selection.visible) {
          this.selection.visible = true;
        }
        if (!this.selection.visible) {
          return;
        }
        const { left, top } = this.getScrollOffset();
        this.selection.endLeft = evt.offsetX + left;
        this.selection.endTop = evt.offsetY + top;
        this.calculateNodeSelection();
      }
    },
    onMouseUp(evt: MouseEvent) {
      if (this.selection.visible) {
        this.calculateNodeSelection();
        const { left, top } = this.getScrollOffset();
        this.selection.endLeft = evt.offsetX + left;
        this.selection.endTop = evt.offsetY + top;

        this.selection.visible = false;
        this.selectionEnabled = false;
      }
    },
    calculateNodeSelection() {
      this.selectedItems.clear();
      for (const node of this.items) {
        const left = this.getNode(node.id).left;
        const top = this.getNode(node.id).top;

        if (
          Math.min(this.selection.startLeft, this.selection.endLeft) < left &&
          left < Math.max(this.selection.startLeft, this.selection.endLeft)
        ) {
          if (
            Math.min(this.selection.startTop, this.selection.endTop) < top &&
            top < Math.max(this.selection.startTop, this.selection.endTop)
          ) {
            this.selectedItems.add(node.id);
          }
        }
      }
    },
  },
  computed: {
    selected: function () {
      return (nodeID: string) => {
        return this.selectedItems.has(nodeID);
      };
    },
  },
  watch: {
    selectedItems: {
      deep: true,
      handler: function (this: any) {
        try {
          for (const node of this.items) {
            this.plumbInstance.removeFromPosse(node.id, "drag");
          }
        } catch (err) {
          console.error(err);
        }
        this.selectedItems.forEach((elemID: string) => {
          this.plumbInstance.addToPosse(elemID, "drag");
        });
      },
    },
  },
  beforeUnmount() {
    this.unmounted = true;
  },
  mounted() {
    this.plumbInstance = jsPlumb.getInstance({});
    this.plumbInstance.importDefaults({
      PaintStyle: {
        strokeWidth: 3,
        stroke: currentColorScheme().flowchart.lineStroke,
      },
      // Connector: "Straight",
      Connector: "Flowchart",
      ConnectionOverlays: [
        ["Arrow", { location: 0.45, width: 8, foldback: 1 }],
      ], // 之所以Location不是0.5,是为了避开中间可能的拐点。
    });
    this.plumbInstance.setContainer("diagramContainer");
    this.plumbInstance.ready(async () => {
      this.statusMgr.setPlumb(this, this.plumbInstance);
      this.plumbInstance.bind("click", (conn: Connection, e: Event) => {
        const evt = e as PointerEvent;
        if (evt.metaKey || evt.ctrlKey) {
          this.statusMgr.straighten(this, conn);
        } else if (evt.shiftKey) {
          this.statusMgr.deleteEdgeByConnection(this, conn);
        } else {
          onEdgeClick(this, conn, e);
        }
      });
    });
    getCards();
    setCanvasOnNewNodeCallback((nodeType: string) => {
      const newUUID = this.statusMgr.newUUID(nodeType);
      this.addNode(newUUID, 100, 100, nodeType, [], false, [], []);
    });

    this.plumbInstance.bind(
      "connection",
      (newConn: ConnectionMadeEventInfo, e: Event) => {
        // 仅捕捉由用户操作发出的事件。因此如果e为null，说明是程序引发的连接，不用处理。
        if (e == null) {
          return;
        }
        const newEdge: Edge = {
          id: this.statusMgr.newEdgeUUID(),
          srcEndpointID: parseEndpointID(
            (newConn.sourceEndpoint as any).getUuid()
          ).endpointID,
          srcNode: newConn.sourceId,
          destEndpointID: parseEndpointID(
            (newConn.targetEndpoint as any).getUuid()
          ).endpointID,
          destNode: newConn.targetId,
          midpoints: [],
        };
        this.statusMgr.addEdge(newEdge);
        bindConnEvents(newConn.connection);
      }
    );

    addOnNodeParamsChanged(
      (newParams: {
        nodeID: string;
        params: { name: string; value: unknown }[];
      }) => {
        if (this.unmounted) {
          console.error("This canvas has been unmounted");
          return;
        }
        const node = this.items.find(
          (node: Node) => node.id == newParams.nodeID
        );
        if (node == null) {
          throw Error("Node is null");
        }
        node.params = newParams.params;
      }
    );
    setOnRun(async () => {
      const fcJSON = dumpFlowchart(this.items, this.statusMgr);
      await run(fcJSON);
      ElMessage.success("Simulation started!");
    });

    setOnNodeFlipped((nodeID: string) => {
      this.statusMgr.flipNode(this, nodeID);
    });

    setOnNodeDeleteRequest((nodeID: string) => {
      this.delNode(nodeID);
    });

    setOnHighlightPort((endpointID: string) => {
      this.highlightEndpoint(endpointID);
    });

    setOnEditPortsOnNode((nodeID: string, direction: "input" | "output") => {
      ElMessageBox.prompt(
        tr(
          'Set ports (with "," as separator, and obeying C/C++ variable name rules).'
        ),

        tr("Set the ports"),
        {
          inputValue:
            direction == "input"
              ? this.statusMgr.getNode(nodeID).inputPorts.join(",")
              : this.statusMgr.getNode(nodeID).outputPorts.join(","),
          confirmButtonText: "OK",
          cancelButtonText: "Cancel",
          inputPattern: /([a-zA-Z_][a-zA-Z0-9_]*,)*([a-zA-Z_][a-zA-Z0-9_]*)/,
          inputErrorMessage: "Invalid port representation",
        }
      )
        .then(({ value }) => {
          const ports = value.split(",");
          for (let i in ports) {
            ports[i] = ports[i].trim();
          }
          this.statusMgr.modifyPorts(nodeID, ports, direction);
        })
        .catch(() => {
          ElMessage({
            type: "info",
            message: "Input canceled",
          });
        });
    });
  },
});
</script>
